/*-------------------------------------------------------------------------
 *
 * generic_xlog.c
 *	 实现通用 XLOG 记录。
 *
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 * src/backend/access/transam/generic_xlog.c
 *
 *-------------------------------------------------------------------------
 */
#include "postgres.h"

#include "access/bufmask.h"
#include "access/generic_xlog.h"
#include "access/xlogutils.h"
#include "miscadmin.h"
#include "utils/memutils.h"

/*-------------------------------------------------------------------------
 * 在内部，页之间的增量由一组片段组成。每个片段表示在页的特定区域所做的更改。片段的组成如下：
 *
 * - 页区域的偏移量 (OffsetNumber)
 * - 页区域的长度 (OffsetNumber)
 * - 数据 - 要放入该区域的数据（长度指定的字节数）
 *
 * 页的未更改区域在其增量中不被表示。因此，增量可能比完整的页图像更紧凑。但是，如果两个片段之间的未更改区域小于片段头（偏移量+长度），则在增量的整体大小方面并不划算。因此，如果它们之间的未更改区域 <= MATCH_THRESHOLD 字节，我们合并相邻的片段。
 *
 * 我们不会在页的“下部”和“上部”之间合并片段；pd_lower 和 pd_upper 以内很少有在 MATCH_THRESHOLD 字节之间的情况，处理这种不频繁的情况会不必要地使增量计算代码变得复杂和减慢。因此，最坏情况下的增量大小包括两个片段头加上一个完整页面的数据。
 *-------------------------------------------------------------------------
 */
#define FRAGMENT_HEADER_SIZE	(2 * sizeof(OffsetNumber))
#define MATCH_THRESHOLD			FRAGMENT_HEADER_SIZE
#define MAX_DELTA_SIZE			(BLCKSZ + 2 * FRAGMENT_HEADER_SIZE)

/* 单页的通用 xlog 数据结构 */
typedef struct
{
	Buffer		buffer;			/* 注册的缓冲区 */
	int			flags;			/* 此缓冲区的标志 */
	int			deltaLen;		/* 在增量字段中消耗的空间 */
	char	   *image;			/* 用于修改的页图像副本，不能就地操作以确保内存块对齐 */
	char		delta[MAX_DELTA_SIZE];	/* 页图像之间的增量 */
} PageData;

/* 通用 xlog 记录构造的状态 */
struct GenericXLogState
{
	/* 关于每个页面的信息，见上文 */
	PageData	pages[MAX_GENERIC_XLOG_PAGES];
	bool		isLogged;
	/* 页图像（经过正确对齐） */
	PGAlignedBlock images[MAX_GENERIC_XLOG_PAGES];
};

static void fc_writeFragment(PageData *fc_pageData, OffsetNumber fc_offset,
						  OffsetNumber fc_len, const char *fc_data);
static void fc_computeRegionDelta(PageData *fc_pageData,
							   const char *fc_curpage, const char *fc_targetpage,
							   int fc_targetStart, int fc_targetEnd,
							   int fc_validStart, int fc_validEnd);
static void fc_computeDelta(PageData *fc_pageData, Page fc_curpage, Page fc_targetpage);
static void fc_applyPageRedo(Page fc_page, const char *fc_delta, Size fc_deltaSize);


/*
 * 将下一个片段写入 pageData 的增量。
 *
 * 该片段具有给定的偏移量和长度，数据指向实际数据（长度为 length）。
 */
static void fc_writeFragment(PageData *fc_pageData, OffsetNumber fc_offset, OffsetNumber fc_length,
			  const char *fc_data)
{
	char	   *fc_ptr = fc_pageData->delta + fc_pageData->deltaLen;

	/* 验证我们是否有足够的空间 */
	Assert(fc_pageData->deltaLen + sizeof(fc_offset) +
		   sizeof(fc_length) + fc_length <= sizeof(fc_pageData->delta));

	/* 写入片段数据 */
	memcpy(fc_ptr, &fc_offset, sizeof(fc_offset));
	fc_ptr += sizeof(fc_offset);
	memcpy(fc_ptr, &fc_length, sizeof(fc_length));
	fc_ptr += sizeof(fc_length);
	memcpy(fc_ptr, fc_data, fc_length);
	fc_ptr += fc_length;

	fc_pageData->deltaLen = fc_ptr - fc_pageData->delta;
}

/*
 * 计算将 curpage 的一个区域转换为 targetpage 相应区域所需的 XLOG 片段，并将它们附加到 pageData 的增量字段。要转换的区域从 targetStart 到 targetEnd-1。curpage 中无效的字节范围在 validStart 到 validEnd-1 之外，应视为无效，并始终用目标数据覆盖。
 *
 * 这个函数是一个热点，因此在数据匹配循环中尽可能地严谨是值得的。
 */
static void fc_computeRegionDelta(PageData *fc_pageData,
				   const char *fc_curpage, const char *fc_targetpage,
				   int fc_targetStart, int fc_targetEnd,
				   int fc_validStart, int fc_validEnd)
{
	int			fc_i,
				fc_loopEnd,
				fc_fragmentBegin = -1,
				fc_fragmentEnd = -1;

	/* 通过将任何无效的起始区域包含在第一个片段中来处理 */
	if (fc_validStart > fc_targetStart)
	{
		fc_fragmentBegin = fc_targetStart;
		fc_targetStart = fc_validStart;
	}

	/* 我们将在主循环后处理任何无效的结束区域 */
	fc_loopEnd = Min(fc_targetEnd, fc_validEnd);

	/* 检查所有潜在匹配的字节 */
	fc_i = fc_targetStart;
	while (fc_i < fc_loopEnd)
	{
		if (fc_curpage[fc_i] != fc_targetpage[fc_i])
		{
			/* 在未匹配的字节上，如果不在一个片段中，则开始一个新片段 */
			if (fc_fragmentBegin < 0)
				fc_fragmentBegin = fc_i;
			/* 将未匹配数据的端点标记为不确定 */
			fc_fragmentEnd = -1;
			/* 在紧密循环中尽可能地扩展片段 */
			fc_i++;
			while (fc_i < fc_loopEnd && fc_curpage[fc_i] != fc_targetpage[fc_i])
				fc_i++;
			if (fc_i >= fc_loopEnd)
				break;
		}

		/* 找到匹配的字节，因此记住未匹配片段的结束 */
		fc_fragmentEnd = fc_i;

		/*
		 * 尽可能在紧密循环中扩展匹配。  （在典型工作负载中，这个内部循环占用了这个函数的大部分运行时间。）
		 */
		fc_i++;
		while (fc_i < fc_loopEnd && fc_curpage[fc_i] == fc_targetpage[fc_i])
			fc_i++;

		/*
		 * 此时有几种可能的情况：
		 *
		 * 1. 我们没有未写入的碎片（fragmentBegin < 0）。  没有什么可以写的；而且 fragmentEnd 是什么都无所谓。
		 *
		 * 2. 我们找到了超过 MATCH_THRESHOLD 连续匹配的字节。  输出未写入的碎片，停止在 fragmentEnd。
		 *
		 * 3. 匹配扩展到 loopEnd。  我们在这里不做任何事情，退出循环，然后输出未写入的碎片，如果有的话，将其与无效的结束区域合并。如果不这样合并，fragmentEnd 确定最终 writeFragment 调用需要写多少。
		 *
		 * 4. 我们在 loopEnd 之前找到了一个未匹配的字节。  循环将重复，并进入上面的未匹配字节段。因此在这种情况下，fragmentEnd 是什么也无所谓。  匹配的字节将合并到继续的未匹配碎片中。
		 *
		 * 只有在情况 3 中，我们才以有意义的 fragmentEnd 值到达循环底部，这就是为什么我们在上面无条件地分配 "fragmentEnd = i" 是可以的。
		 */
		if (fc_fragmentBegin >= 0 && fc_i - fc_fragmentEnd > MATCH_THRESHOLD)
		{
			fc_writeFragment(fc_pageData, fc_fragmentBegin,
						  fc_fragmentEnd - fc_fragmentBegin,
						  fc_targetpage + fc_fragmentBegin);
			fc_fragmentBegin = -1;
			fc_fragmentEnd = -1;	/* 实际上并不是必要的 */
		}
	}

	/* 通过将任何无效结束区域包含在最终碎片中来处理它 */
	if (fc_loopEnd < fc_targetEnd)
	{
		if (fc_fragmentBegin < 0)
			fc_fragmentBegin = fc_loopEnd;
		fc_fragmentEnd = fc_targetEnd;
	}

	/* 如果有的话，写入最终碎片 */
	if (fc_fragmentBegin >= 0)
	{
		if (fc_fragmentEnd < 0)
			fc_fragmentEnd = fc_targetEnd;
		fc_writeFragment(fc_pageData, fc_fragmentBegin,
					  fc_fragmentEnd - fc_fragmentBegin,
					  fc_targetpage + fc_fragmentBegin);
	}
}

/*
 * 计算将 curpage 转换为 targetpage 所需的 XLOG delta 记录，
 * 并将其存储在 pageData 的 delta 字段中。
 */
static void fc_computeDelta(PageData *fc_pageData, Page fc_curpage, Page fc_targetpage)
{
	int			fc_targetLower = ((PageHeader) fc_targetpage)->pd_lower,
				fc_targetUpper = ((PageHeader) fc_targetpage)->pd_upper,
				fc_curLower = ((PageHeader) fc_curpage)->pd_lower,
				fc_curUpper = ((PageHeader) fc_curpage)->pd_upper;

	fc_pageData->deltaLen = 0;

	/* 计算页面下部的 delta 记录 ... */
	fc_computeRegionDelta(fc_pageData, fc_curpage, fc_targetpage,
					   0, fc_targetLower,
					   0, fc_curLower);
	/* ... 并计算上部，忽略中间部分 */
	fc_computeRegionDelta(fc_pageData, fc_curpage, fc_targetpage,
					   fc_targetUpper, BLCKSZ,
					   fc_curUpper, BLCKSZ);

	/*
	 * 如果启用了 xlog 调试，则检查生成的 delta。  将 delta 应用到 curpage 的结果应等同于 targetpage。
	 */
#ifdef WAL_DEBUG
	if (XLOG_DEBUG)
	{
		PGAlignedBlock fc_tmp;

		memcpy(fc_tmp.data, fc_curpage, BLCKSZ);
		fc_applyPageRedo(fc_tmp.data, fc_pageData->delta, fc_pageData->deltaLen);
		if (memcmp(fc_tmp.data, fc_targetpage, fc_targetLower) != 0 ||
			memcmp(fc_tmp.data + fc_targetUpper, fc_targetpage + fc_targetUpper,
				   BLCKSZ - fc_targetUpper) != 0)
			elog(ERROR, "result of generic xlog apply does not match");
	}
#endif
}

/*
 * 开始针对指定关系的修改创建新的通用 xlog 记录。
 */
GenericXLogState *
GenericXLogStart(Relation fc_relation)
{
	GenericXLogState *fc_state;
	int			fc_i;

	fc_state = (GenericXLogState *) palloc(sizeof(GenericXLogState));
	fc_state->isLogged = RelationNeedsWAL(fc_relation);

	for (fc_i = 0; fc_i < MAX_GENERIC_XLOG_PAGES; fc_i++)
	{
		fc_state->pages[fc_i].image = fc_state->images[fc_i].data;
		fc_state->pages[fc_i].buffer = InvalidBuffer;
	}

	return fc_state;
}

/*
 * 为通用 xlog 记录注册新缓冲区。
 *
 * 返回指向 GenericXLogState 中页面图像的指针，调用者应修改其内容。
 *
 * 如果缓冲区已注册，则只需返回其现有条目。
 * （在这种情况下如何处理标志还不太清楚，但目前我们继续使用原始标志。）
 */
Page GenericXLogRegisterBuffer(GenericXLogState *fc_state, Buffer fc_buffer, int fc_flags)
{
	int			fc_block_id;

	/* 在数组中搜索现有条目或第一个未使用的插槽 */
	for (fc_block_id = 0; fc_block_id < MAX_GENERIC_XLOG_PAGES; fc_block_id++)
	{
		PageData   *fc_page = &fc_state->pages[fc_block_id];

		if (BufferIsInvalid(fc_page->buffer))
		{
			/* 空槽，因此使用它（后面不会有匹配） */
			fc_page->buffer = fc_buffer;
			fc_page->flags = fc_flags;
			memcpy(fc_page->image, BufferGetPage(fc_buffer), BLCKSZ);
			return (Page) fc_page->image;
		}
		else if (fc_page->buffer == fc_buffer)
		{
			/*
			 * 缓冲区已经注册。  只需返回已经准备好的图像。
			 */
			return (Page) fc_page->image;
		}
	}

	elog(ERROR, "maximum number %d of generic xlog buffers is exceeded",
		 MAX_GENERIC_XLOG_PAGES);
	/* 保持编译器安静 */
	return NULL;
}

/*
 * 将 GenericXLogState 表示的更改应用于实际缓冲区，
 * 并发出通用 xlog 记录。
 */
XLogRecPtr GenericXLogFinish(GenericXLogState *fc_state)
{
	XLogRecPtr	fc_lsn;
	int			fc_i;

	if (fc_state->isLogged)
	{
		/* 已记录的关系：在关键区段中创建 xlog 记录。 */
		XLogBeginInsert();

		START_CRIT_SECTION();

		/*
		 * 如果需要，计算 delta，写入缓冲区，更改缓冲区状态为脏，并注册更改。
		 */
		for (fc_i = 0; fc_i < MAX_GENERIC_XLOG_PAGES; fc_i++)
		{
			PageData   *fc_pageData = &fc_state->pages[fc_i];
			Page		fc_page;
			PageHeader	fc_pageHeader;

			if (BufferIsInvalid(fc_pageData->buffer))
				continue;

			fc_page = BufferGetPage(fc_pageData->buffer);
			fc_pageHeader = (PageHeader) fc_pageData->image;

			/*
			 * 在我们仍然拥有未修改的页面和新图像时计算 delta。 如果我们正在记录完整图像，则不需要此步骤。
			 */
			if (!(fc_pageData->flags & GENERIC_XLOG_FULL_IMAGE))
				fc_computeDelta(fc_pageData, fc_page, (Page) fc_pageData->image);

			/*
			 * 应用图像，注意将 pd_lower 和 pd_upper 之间的“洞”归零，以避免实际页面状态与重放生成的状态之间的偏差。
			 */
			memcpy(fc_page, fc_pageData->image, fc_pageHeader->pd_lower);
			memset(fc_page + fc_pageHeader->pd_lower, 0,
				   fc_pageHeader->pd_upper - fc_pageHeader->pd_lower);
			memcpy(fc_page + fc_pageHeader->pd_upper,
				   fc_pageData->image + fc_pageHeader->pd_upper,
				   BLCKSZ - fc_pageHeader->pd_upper);

			MarkBufferDirty(fc_pageData->buffer);

			if (fc_pageData->flags & GENERIC_XLOG_FULL_IMAGE)
			{
				XLogRegisterBuffer(fc_i, fc_pageData->buffer,
								   REGBUF_FORCE_IMAGE | REGBUF_STANDARD);
			}
			else
			{
				XLogRegisterBuffer(fc_i, fc_pageData->buffer, REGBUF_STANDARD);
				XLogRegisterBufData(fc_i, fc_pageData->delta, fc_pageData->deltaLen);
			}
		}

		/* 插入 xlog 记录 */
		fc_lsn = XLogInsert(RM_GENERIC_ID, 0);

		/* 设置 LSN */
		for (fc_i = 0; fc_i < MAX_GENERIC_XLOG_PAGES; fc_i++)
		{
			PageData   *fc_pageData = &fc_state->pages[fc_i];

			if (BufferIsInvalid(fc_pageData->buffer))
				continue;
			PageSetLSN(BufferGetPage(fc_pageData->buffer), fc_lsn);
		}
		END_CRIT_SECTION();
	}
	else
	{
		/* 无日志关系：跳过与 xlog 相关的内容 */
		START_CRIT_SECTION();
		for (fc_i = 0; fc_i < MAX_GENERIC_XLOG_PAGES; fc_i++)
		{
			PageData   *fc_pageData = &fc_state->pages[fc_i];

			if (BufferIsInvalid(fc_pageData->buffer))
				continue;
			memcpy(BufferGetPage(fc_pageData->buffer),
				   fc_pageData->image,
				   BLCKSZ);
			/* 在这种情况下，我们不担心将“洞”归零 */
			MarkBufferDirty(fc_pageData->buffer);
		}
		END_CRIT_SECTION();
		/* 在这种情况下，我们没有 LSN 可以返回 */
		fc_lsn = InvalidXLogRecPtr;
	}

	pfree(fc_state);

	return fc_lsn;
}

/*
 * 中止通用 xlog 记录构建。缓冲区没有进行任何更改。
 *
 * 注意：如果需要，调用者负责释放缓冲区上的锁/钉子。
 */
void GenericXLogAbort(GenericXLogState *fc_state)
{
	pfree(fc_state);
}

/*
 * 将增量应用到给定页面图像。
 */
static void fc_applyPageRedo(Page fc_page, const char *fc_delta, Size fc_deltaSize)
{
	const char *fc_ptr = fc_delta;
	const char *fc_end = fc_delta + fc_deltaSize;

	while (fc_ptr < fc_end)
	{
		OffsetNumber fc_offset,
					fc_length;

		memcpy(&fc_offset, fc_ptr, sizeof(fc_offset));
		fc_ptr += sizeof(fc_offset);
		memcpy(&fc_length, fc_ptr, sizeof(fc_length));
		fc_ptr += sizeof(fc_length);

		memcpy(fc_page + fc_offset, fc_ptr, fc_length);

		fc_ptr += fc_length;
	}
}

/*
 * 通用 xlog 记录的重做函数。
 */
void generic_redo(XLogReaderState *fc_record)
{
	XLogRecPtr	fc_lsn = fc_record->EndRecPtr;
	Buffer		fc_buffers[MAX_GENERIC_XLOG_PAGES];
	uint8		fc_block_id;

	/* 保护 buffers[] 数组的有限大小 */
	Assert(XLogRecMaxBlockId(fc_record) < MAX_GENERIC_XLOG_PAGES);

	/* 迭代块 */
	for (fc_block_id = 0; fc_block_id <= XLogRecMaxBlockId(fc_record); fc_block_id++)
	{
		XLogRedoAction fc_action;

		if (!XLogRecHasBlockRef(fc_record, fc_block_id))
		{
			fc_buffers[fc_block_id] = InvalidBuffer;
			continue;
		}

		fc_action = XLogReadBufferForRedo(fc_record, fc_block_id, &fc_buffers[fc_block_id]);

		/* 如有必要，将重做应用于给定块 */
		if (fc_action == BLK_NEEDS_REDO)
		{
			Page		fc_page;
			PageHeader	fc_pageHeader;
			char	   *fc_blockDelta;
			Size		fc_blockDeltaSize;

			fc_page = BufferGetPage(fc_buffers[fc_block_id]);
			fc_blockDelta = XLogRecGetBlockData(fc_record, fc_block_id, &fc_blockDeltaSize);
			fc_applyPageRedo(fc_page, fc_blockDelta, fc_blockDeltaSize);

			/*
			 * 由于增量不包含 pd_lower 和 pd_upper 之间“洞”中的信息，将其设置为零，以确保我们生成与
			 * GenericXLogFinish 应用日志操作相同的页面状态。
			 */
			fc_pageHeader = (PageHeader) fc_page;
			memset(fc_page + fc_pageHeader->pd_lower, 0,
				   fc_pageHeader->pd_upper - fc_pageHeader->pd_lower);

			PageSetLSN(fc_page, fc_lsn);
			MarkBufferDirty(fc_buffers[fc_block_id]);
		}
	}

	/* 更改完成：解锁并释放所有缓冲区 */
	for (fc_block_id = 0; fc_block_id <= XLogRecMaxBlockId(fc_record); fc_block_id++)
	{
		if (BufferIsValid(fc_buffers[fc_block_id]))
			UnlockReleaseBuffer(fc_buffers[fc_block_id]);
	}
}

/*
 * 在对通用页面进行一致性检查之前对其进行屏蔽。
 */
void generic_mask(char *fc_page, BlockNumber fc_blkno)
{
	mask_page_lsn_and_checksum(fc_page);

	mask_unused_space(fc_page);
}
